﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Net.Sockets;
using System.Threading;
using System.Net;
using System.Xml.Serialization;
using System.IO;

    /* 
     * Server: Communicates to clients and recieves and stores account information and chat history
     */

namespace ChatServer
{
    
    public partial class Server : Form
    {
        /*
         * Contains static methods and recieves connections, assigning each new client to a client handler.
         * 
         * Form contains a textbox for printing messages and start and stop buttons (button1 and button2 respectively).
         * Server starts at button1_click
         */

        // static variables correspond to data stored in text files
        public static List<chat> chats = new List<chat>(); // all chats (each chat is a conversation between users)
        public static List<Account> accounts = new List<Account>(); // all users of the system

        // variables specific to Server class
        TcpListener listener; // connects to clients
        Thread mainThread; // contains methoad that waits for clients
        delegate void SetTextCallback(string text); // allows setting textbox value


        // Static methods for general functions and editing static variables
        
        public static String Merge(List<String> list)
        {
            // method for converting list to string for use in various places
            try
            {
                String _string = list[0]; // string starts as first term in list
                for (int counter = 1; counter <= list.IndexOf(list.Last<String>()); counter++) // loops through the list
                {
                    _string = _string + "\r\n" + list[counter]; // appends each successive term of the list to the string, in a new line
                }
                return _string; //returns the string
            }
            catch (ArgumentOutOfRangeException) { return ""; } // if the list is empty and list[0] is therefore out of range, a blank string will be returned
        }

        public static void AddAccount(String username, String password) 
            //adds new account to the static accounts list
        {
            int index; //position of new account in list
            try
            {
                index = accounts.Last<Account>().GetIndex() + 1; //position set to one after last position
            }
            catch (ArgumentNullException)
            {
                index = 0; // if list is empty adds account to beggining
            }
            catch (InvalidOperationException)
            {
                index = 0; // if list is empty adds account to beggining
            } 
            Account add = new Account(); //creates a new account
            add.Initialize(username,password,index); // gives the account the username and password provided by the user and the index calculated
            accounts.Add(add); //appends account to list
        }
        public static int AddChat(int accountIndex)
        {
            //adds new chat to list of chats 
            //account_index is ID of user making the chat

            //same as with add account
            int index;
            try
            {
                index = chats.Last<chat>().GetID() + 1;
            }
            catch (ArgumentNullException )
            {
                index = 0;
            }
            catch (InvalidOperationException)
            {
                index = 0;
            }
                chat add = new chat();
                add.SetID(index);
                chats.Add(add);

            //makes the user making the chat a member of the chat

                accounts[accountIndex].chats.Add(index); //adds chat ID to list of chats of user
                chats[index].members.Add(accountIndex); //adds user ID to list of members of char
                chats[index].memberNames.Add(Server.accounts[accountIndex].GetUsername()); //adds user's name to list of chat member names
                
            accounts[accountIndex].chatnames.Add(String.Join(" , ", chats[index].memberNames.ToArray())); 
                         //adds a list of the members of the chat (currently just this user) to list chat names of user

                return index; //returns the position of the chat
        }
        public static Account GetAccount(int index) //returns account object for user of any ID
        {
            return accounts[index];
        }
        public static Account GetAccount(String user)  //returns account object for user of any name
        {
            try{
            return accounts.Find(new Predicate<Account>(item => item.GetUsername().CompareTo(user) == 0)); //uses find method to search for name
            }
            catch(ArgumentNullException){return null;} //returns null if no user with the name given exists
        }
        public static void AddChat(chat Chat) //adds a chat already in object form
        {
            chats.Add(Chat);
        }
        public static chat GetChat(int index) //returns chat object for any ID
        {
            return chats[index];
        }
        
        //static methods for saving and loading from text files
        public static List<chat> LoadChats() 
        {
            //loads chats from text files to chats list
            XmlSerializer formatter = new XmlSerializer(typeof(List<chat>));  //xml to list of chat converter
            FileStream stream = new FileStream("chat.txt", FileMode.Open, FileAccess.Read, FileShare.Read); //reads xml from file
            List<chat> answer = (List<chat>)formatter.Deserialize(stream); //converter converts xml to list of chat objects
            stream.Close();
            return answer;
            
        }
        public static List<Account> LoadAccounts()
        {
            //loads accounts from txt file
            //same as for load chats
            XmlSerializer formatter = new XmlSerializer(typeof(List<Account>));
            FileStream stream = new FileStream("accounts.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
            List<Account> answer = (List<Account>)formatter.Deserialize(stream);
            stream.Close();
            return answer;
        }
        public static void SaveAccounts(List<Account> list)
        {
            //saves accounts from accounts file to txt file
            XmlSerializer formatter = new XmlSerializer(typeof(List<Account>)); //can convert from list of accounts to xml
            TextWriter writer = new StreamWriter("accounts.txt"); // can write to file
            formatter.Serialize(writer, list); // does the conversion
            writer.Close(); // writes xml to file
        } 
        public static void SaveChats(List<chat> list) 
        {
            //saves chats to chat.txt
            //same as above method
            XmlSerializer formatter = new XmlSerializer(typeof(List<chat>));
            TextWriter writer = new StreamWriter("chat.txt");
            formatter.Serialize(writer, list);
            writer.Close();
        } 
        
        
        
        //methods for Server object
        public Server()
        { 
            //when Server is launched
            InitializeComponent(); //creates interface of textbox and buttons based on xaml
            textBox1.Text = "Server on."; // sends first message 
            accounts = LoadAccounts();
            chats = LoadChats(); //loads accounts and chats from file
        }
        
        
        public void print(String message)
        {
            //writes to text box
            if (this.textBox1.InvokeRequired) //if called from different thread
            {
                SetTextCallback d = new SetTextCallback(print); // creates delegate for this method
                this.Invoke(d, new object[] { message }); // calls method again on correct thread
            }
            else // if called from right thread
            {
                textBox1.AppendText("\r\n" + message); // adds message to a new line on the text box
            }
        }

       
        private void button1_Click(object sender, EventArgs e) //called when Start button is pressed
        {
            button1.Enabled = false; // disables start button
            button2.Enabled = true; // enables stop button
            print("Server started.");
            listener = new TcpListener(IPAddress.Any, 3000); // when started, listener listens for connections 
                                                            //       from any IP address on a specified port (3000)
           
            mainThread = new Thread(new ThreadStart(WaitForClient));
            mainThread.Start(); //waits for clients on a new thread
        }
        private void WaitForClient()
        {
            // Waits for clients to connect to server and runs ClientHandler method for each new client to connect
            try
            {
                listener.Start(); //starts listener
                while (true) // keeps waiting for clients indefinetely
                {
                    print("Searching for clients...");

                    TcpClient client = listener.AcceptTcpClient(); // waits for client 
                    // for each client found: TcpClient object created

                    print("Client logged on.");

                    //Each client found is handled on a new thread
                    Thread ClientThread = new Thread(new ParameterizedThreadStart(ClientHandler)); // new thread created for client to be handled on
                    ClientThread.Start(client); // begins handling this client with client handler method

                } //after client sent to client handler, loops back and searches for more clients
            }
            catch (SocketException) { print("Server stopped."); } //socket exception occurs if listener has been stopped by pressing stop button

        }
        
        private void ClientHandler(object TcpClient)
        {
            //handles each client

            TcpClient client = (TcpClient)TcpClient; //gets tcp client sent by WaitForClient

            //for each client a ClientHandler object is created
            ClientHandler handler = new ClientHandler(client, client.GetStream(), this); //client handler is given the client, its NetworkStream 
                                                                                         // and this server instance (for printing messages)
        }

        private void button2_Click(object sender, EventArgs e) //run when stop button pressed
        {
            listener.Stop();
            listener = null; //stops and empties the listener to close connection and call socketexception in WaitForClient method
            button2.Enabled = false; // disables stop button
            button1.Enabled = true; // enables start button
        }
    }

    //An instance of this class is created for each client that logs on to the server.
    class ClientHandler
    {
        //handles each client

        //connection information
        TcpClient client; //connection to client
        NetworkStream stream; //stream from client
        Server server; //server

        //chat related information
        int accountIndex; //user that client is logged in as (if any)
        int chatId; //chat currently open (if any)
        bool loggedin = false; // whether client is logged in as a user - originally false

        
        public ClientHandler(TcpClient _client, NetworkStream _stream, Server _server)
        {
            //creates new instance


            client = _client;
            stream = _stream;
            server = _server; //sets connection variables sent by server

            //This bit accepts messages from the client
            try
            {
                while (true) // can recieve messages indefinately
                {
                    Write(stream, "go"); // sends 'go' to tell client it is ready to recieve a message

                    String message = Read(stream); //reads a message from the client
                    print(message);
                    if (!loggedin)
                    {
                        //messages accepted when not logged in
                        // log in (instruction), new account (instruction), logged in (question)
                        if (message.CompareTo("log in") == 0)
                        {
                            login(); //logs in on instruction from client
                        }
                        if (message.CompareTo("new account") == 0)
                        {
                            NewAccount(); //creates new account
                        }
                        if (message.CompareTo("logged in") == 0)
                        {
                            Write(stream, "no"); //informs client that it is not logged in
                            Read(stream); // receives OK message from client
                        }
                    }
                    else
                    {
                        //messages accepted when logged in (these cannot be accepted if not logged in for obvious security reasons)
                        if (message.CompareTo("logged in") == 0)
                        {
                            if (loggedin && Server.accounts[accountIndex].loggedin) //verifies that client has indeed logged in
                            {
                                Write(stream, "yes"); //answers yes
                            }
                            else
                            {
                                Write(stream, "no"); //answers no
                            }
                            Read(stream); // recieves 'OK' message from client - cannot move on unless message recieved
                        }
                        if (message.CompareTo("log out") == 0)
                        {
                            loggedin = false;
                            Server.accounts[accountIndex].loggedin = false; //logs out both in client handler and in the accounts list
                            Server.SaveAccounts(Server.accounts); //saves accounts as after every change to accounts
                        }
                        if (message.CompareTo("change password") == 0)
                        {
                            ChangePass(); //change password
                        }
                        if (message.CompareTo("get account") == 0) //sends entire account file to client
                        {
                            UpdateChatNames(); //updates chat names    
                            SendAccount(); //sends file
                        }


                        //divert commands directly to methods
                        if (message.CompareTo("get person") == 0)
                        {
                            GetPerson(); 
                        }
                        if (message.CompareTo("get chat") == 0)
                        {
                            GetChat();
                        }
                        if (message.CompareTo("new friend") == 0)
                        {
                            AddFriend();
                        }


                        if (message.CompareTo("new chat") == 0) 
                        {
                            //creates new chat and opens it
                            chatId = Server.AddChat(accountIndex); // adds chat to chats and obtains its index, making it the current chat
                            Server.SaveChats(Server.chats);
                            Server.SaveAccounts(Server.accounts); //saves accounts and chats
                            Write(stream, "OK"); //verifies to client that method has ran successfully
                        }
                       
                        if (message.CompareTo("chat") == 0)
                        {
                            //allows user to open existing chat
                            Write(stream, "id");
                            chatId = Convert.ToInt32(Read(stream)); //obtains id from client and makes it current chat
                            Write(stream, "OK"); //verifies to client that method has completed successfully
                        }
                        
                        //these commands are accepted while chatting
                        if (message.CompareTo("add member") == 0)
                        {
                            AddMember(chatId); //adds new member to current chat
                            Write(stream, "OK");
                        }

                        if (message.CompareTo("say") == 0)
                        {
                            Server.chats[chatId].AddLine(Server.accounts[accountIndex].GetUsername() + ": " + Read(stream));
                            //message from client is added to chat history
                            Server.SaveChats(Server.chats); //saves chats
                        }                  
                        
                    }
                }
            }
            catch (NoMessageException) { } //Exception thrown by various methods called here when there is evidence that client has logged off
            loggedin = false; //logs out
            print("Client logged off. \r\nSearching for clients...");
            client.Close(); //ends connection
        }

        private void UpdateChatNames()
        {
            //updates chat names
            for (int c = 0; c < Server.accounts[accountIndex].chats.Count(); c++) //loops through user's chats
            {
                try
                {
                    Server.accounts[accountIndex].chatnames[c] = (String.Join(" , ", Server.chats[Server.accounts[accountIndex].chats[c]].memberNames.ToArray()));
                    // for each chat, finds it in chats, gets the names of its members, turns them into a string and puts it into accounts
                }
                catch (ArgumentOutOfRangeException) //if index is out of range
                {
                    Server.accounts[accountIndex].chatnames.Add(String.Join(" , ", Server.chats[Server.accounts[accountIndex].chats[c]].memberNames.ToArray()));
                    //adds membernames to the end
                }
            }
            Server.SaveAccounts(Server.accounts); //as always after changing accounts saves accounts
        }
        private void AddMember(int chatId)
        {
            //adds new member to a chat
            Write(stream, "id");
            int memberIndex = Convert.ToInt32(Read(stream)); //asks for member index

            Server.chats[chatId].members.Add(memberIndex); //adds member to chat
            Server.accounts[memberIndex].chats.Add(chatId); //adds chat to member
            Server.chats[chatId].memberNames.Add(Server.accounts[memberIndex].GetUsername()); //adds member name to chat
            UpdateChatNames();//adds chat names to member

            Server.SaveAccounts(Server.accounts);
            Server.SaveChats(Server.chats); //saves accounts and chats
        }
        private void GetPerson()
        {
            //gets person for any id
            Write(stream, "id");
            int index = Convert.ToInt32(Read(stream)); //gets id
            try
            {
                XmlSerializer formatter = new XmlSerializer(typeof(Account));
                formatter.Serialize(stream, Server.accounts[index]); //gets account, converts it to xml and sends it to the client
                Read(stream); //gets ok message
            }
            catch (ArgumentNullException) { Write(stream, "not found"); } //if index does not exist
        }
        private void GetChat()
        {
            //same as get person but for chat
            int id = Convert.ToInt32(Read(stream));
            try
            {
                print("Sending");
                XmlSerializer formatter = new XmlSerializer(typeof(chat));
                formatter.Serialize(stream, Server.chats[id]);
                print("Sent");
                Read(stream);
                print("OK");
            }
            catch (ArgumentNullException) { Write(stream, "not found");  }
        }
        private void AddFriend()
        {
            //adds a user to the current user's list of friends
            Write(stream,"name");
            String name = Read(stream); //gets name of user
            if (!Server.accounts[accountIndex].friends.Any(item => Server.accounts[item].GetUsername().CompareTo(name) == 0)) //if name exists
            {
                try
                {
                    Account account = Server.accounts.Find(new Predicate<Account>(item => item.GetUsername().CompareTo(name) == 0)); //gets account
                    Server.accounts[accountIndex].friends.Add(account.GetIndex()); //adds its index to friends
                    Server.accounts[accountIndex].friendnames.Add(account.GetUsername());// adds name to friendnames
                    Server.accounts[account.GetIndex()].friends.Add(accountIndex); //adds this account to friend's friends
                    Server.accounts[account.GetIndex()].friendnames.Add(Server.accounts[accountIndex].GetUsername()); //adds this user's name to friend's friendnames
                    Write(stream, "OK"); //sends OK message to client
                }

                catch (Exception) { Write(stream, "not found"); }
            }
            else
            {
                Write(stream, "not found"); //if name doesn't exist not found is sent to client
            }
            Server.SaveAccounts(Server.accounts); //save
            Read(stream); // gets 'OK' message from client
        }
        private void ChangePass()
        {
            //changes user's password
            Write(stream, "current");
            String current = Read(stream);
            Write(stream, "new");
            String _new = Read(stream); //gets current and new password from user
            bool answer = Server.accounts[accountIndex].SetPassword(current, _new); //sets password and gets boolean representing success or failure
            Server.SaveAccounts(Server.accounts); // saves
            Write(stream, "" + answer); //returns true for successful and false for unsuccessful
        }
        private void print(String message)
        {
            server.print(message); //prints message on server's text box
        }
        private void login()
        {
               //logs in to account
                print("Log on request recieved.");
                Write(stream, "username");
                print("Waiting for username.");
                String username = Read(stream); //gets username
                Account account = Server.GetAccount(username); //gets account from username
                if (account == null) //if null is returned by getAccount ie. username was not found
                {
                    Write(stream, "wrong username"); //sends wrong username to client
                }
                else
                {
                    Write(stream, "password");
                    String password = Read(stream); //gets password
                    if (account.login(password)) //tries to log in if successful
                    {
                        print("log in true");
                        loggedin = true; //sets loggedin to true
                        accountIndex = account.GetIndex(); //sets account_index variable to index of account logged in to
                    }
                    else //if log in fails at this stage
                    {
                        Write(stream, "wrong password"); //its because of a wrong password
                    }
                }
        }
        private void SendAccount()
        {
            //sends account object to client
            print("Sending account");

            XmlSerializer formatter = new XmlSerializer(typeof(Account));
            formatter.Serialize(stream, Server.accounts[accountIndex]); //gets account, converts it to xml and sends it down the stream to the client

            String RecieveOK = Read(stream); //gets 'OK' message from client
        }
        private void NewAccount()
        {
            //creates new account
            print("New Account request recieved.");
            Write(stream, "username");
            print("Waiting for username.");
            String username = Read(stream); //gets username
            Account account = Server.GetAccount(username); //searches for username in accounts
            if (account == null) //if no such username exists
            {
                Write(stream, "password");
                String password = Read(stream); 
                Write(stream, "repeat");
                String password2 = Read(stream); //gets password twice
                if (password.CompareTo(password2) == 0) //if equal
                {
                    Server.AddAccount(username, password); //adds account
                    Server.SaveAccounts(Server.accounts); //saves accounts
                    login(); //starts log in process
                }
                else //if passwords are different
                {
                    Write(stream, "wrong password");
                    
                }
            }
            else //if username already exists
            {
                Write(stream, "wrong username");       
            }
        }
        

        //methods to read and write messages to and from the client
        private String Read(NetworkStream stream)
        {
            //gets message from client
            byte[] message = new byte[4096]; //byte array to store message
            int bytesRead = 0; //number of bytes read starts at 0
            print("Reading");
            try
            {
                bytesRead = stream.Read(message, 0, 4096); //reads data from stream storing it in 'message'
                //                                             and returning its length to bytesRead

                if (bytesRead == 0) //if no data comes through
                {
                    throw new NoMessageException(); //throws exception which will be caught in constructor
                }
                else // if data came through
                {
                    ASCIIEncoding encoder = new ASCIIEncoding();
                    String answer = (encoder.GetString(message, 0, bytesRead)); //convert data to string
                    return answer; //return string read
                }
            }
            catch (Exception) //if stream cannot be read or doesnt exist
            { 
                throw new NoMessageException(); //throws exception which will be caught in constructor
            }
        }
        private void Write(NetworkStream stream, String message)
        {
            //sends message to client
            try
            {
                ASCIIEncoding encoder = new ASCIIEncoding();
                byte[] buffer = encoder.GetBytes(message); //converts message to byte array
                stream.Write(buffer, 0, buffer.Length); //sends byte array down stream
            }
            catch (Exception) //if writing fails
            {
                throw new NoMessageException(); //excpetion is thrown to be caught in constructor
            }
        }
    }

    //stores data about a user
    //is serializable so it can be converted to xml and sent down a stream to a client or txt file
    [Serializable]
    public class Account
    {
        //user varibales
        public String username; // unique string used to identify account by users
        public int index; // position of account in the accounts array, also a unique identifier of the account
        public String password; //password used to log in
        public List<int> chats = new List<int>(); //list of the id's of all chats account is a member of
        public List<String> chatnames = new List<string>(); //for each chat the account is a member of (and in the same order)
        //                                                   this list contains a string with a list of the members of that chat
        public List<String> friendnames = new List<string>(); //list of the usernames of the accounts contacts
        public List<int> friends = new List<int>(); //list of the index's of the account's contacts


        public bool loggedin = false; //whether any client is currently logged in to this account      
        public bool initialized; //whether the initialize method has been run yet

        public bool login(String pass)
        {
            //logs a user in
            if (pass.CompareTo(password) == 0) //if password is correct
            {
                loggedin = true; //logs in
                return true; // returns success
            }
            else //if password does not match
            {
                return false; //returns failure
            }
        }
        public void Initialize(String user, String pass, int _index)
        {
            //sets the accoutn data
            if (!initialized) //only runs if account has not yet been initialized
            {
                username = user;
                password = pass;
                index = _index; //sets data
                initialized = true; //account is now initialized
            }
        }
        public Account() //when object is first created
        {
            initialized = false; //account has not yet been initialized
        }
        
        //methods to get data from object
        public String GetUsername()
        {
            return username;
        }
        public int GetIndex()
        {
            return index;
        } 


        public bool SetPassword(String current, String _new)
        {
            //allows user to change password
            if (password.CompareTo(current) == 0) //if current password was enered correctly
            {
                password = _new; //changes password
                return true; //success
            }
            else //if current password is wrong
            {
                return false; //failure
            }
        }
    }

    //stores data about each chat
    //serializable so can be sent down streams to file and/or client
    [Serializable]
    public class chat
    {
        public int id; //position in chats list
        public List<int> members = new List<int>(); //indices of member accounts
        public List<String> memberNames = new List<string>(); //usernames of member acocunts in same order
        public List<String> conversation; //chat history
        public chat()
        {
            conversation = new List<String>(); //initializes variable
        }
        
        //methods to get and set data
        public void SetID(int id)
        {
            this.id = id;
        }
        public int GetID()
        {
            return id;
        }
        public String GetConversation()
        {
            return Server.Merge(conversation);
        }

        
        public void AddLine(String line)
        {
            //chat
            conversation.Add(line); //adds text to the conversation
        }
        
        
    }

    //Exception thrown when cannot connect to client
    [Serializable()]
    public class NoMessageException : System.Exception
    {
        public NoMessageException() : base() { }
        public NoMessageException(string message) : base(message) { }
        public NoMessageException(string message, System.Exception inner) : base(message, inner) { }

        // A constructor is needed for serialization when an
        // exception propagates from a remoting server to the client. 
        protected NoMessageException(System.Runtime.Serialization.SerializationInfo info,
            System.Runtime.Serialization.StreamingContext context) { }
    }

}
